/*
 author: FENG Xu, NUNES Thomas
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 
 ====================
 Macros 
 ====================
 */
#define BUFFER_SIZE        1024
#define FILE_NAME_MAX_SIZE 512

#define PORT_DEFAULT        "5000"
#define ADR_DEFAULT 	    "127.0.0.1"

/* nombre de commandes, ERROR non compris */
#define NB_CMD       5
#define MAX_CMD_SIZE 64
#define PROMPT       "client>"
/*
 ====================
 Types 
 ====================
 */
typedef int SOCKET;
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
typedef char* STRING;

typedef enum {
    HELP = 0, LS, PUT, GET, QUIT, ERROR
} COMMAND;
/*
 ====================
 Variables globales 
 ====================
 */
static STRING commands[NB_CMD] = {"help", "ls", "put", "get", "quit"};
/* 
 ====================
 Fonctions 
 ====================
 */
/*
         Client              Serveur
            |                    |
            | dem. de connexion  |
            |------------------->|
            |   acceptation      |
            |<- - - - - - - - -  |
            |                    |
[Traitement]|   indication       |
            |<- - - - - - - - -  |
            |                    |
            |   indication       |
            |------------------->|
            |                    |
            |    commande        |
            |------------------->|
            |   [réponse]        | Traitement
            |<- - - - - - - - -  |
[Traitement]|                    |
            |                    |
            |    deconnexion     |
            |<- - - - - - - - - >|
 */

/*
 * afficher le manuel du shell
 */
void helpShell() {
    printf("\nCommandes disponibles :\n\n");
    printf("help             : afficher ce manuel\n");
    printf("ls               : obtenir la liste des fichiers sous le répertoire courant\n");
    printf("put <nomfichier> : transférer le fichier au serveur (erreur si le fichier n'est pas présent)\n");
    printf("get <nomfichier> : transférer le fichier au client (erreur si le fichier n'est pas présent)\n");
    printf("quit             : terminer le programme\n\n");
}

/*
 * afficher le manuel du programme
 * @param le nom du programme
 */
void helpProgramme(STRING nomPgm) {
    printf("\nUtilisation du programme :\n");
    printf("%s <adresse IP du serveur> <numero de port du serveur>\n", nomPgm);
    printf("Attention: le numero de port doit etre un entier entre 1000 et 99999 inclus.\n\n");
}

static void *xmalloc(size_t size){
	void *p = malloc(size);
	if (!p) printf("erreur d'allocation lors du transfert.");
	return p;
}


static void *xrealloc(void *ptr, size_t size){
	void *p = realloc(ptr, size);
	if (!p) printf("erreur d'allocation lors du transfert.");
	return p;
}

/**
 *lit une ligne de commande et la renvoie sous forme de char*
 */
static char *readline(void){
	size_t buf_len = 16;
	char *buf = xmalloc(buf_len * sizeof(char));
	if (fgets(buf, buf_len, stdin) == NULL) {
		free(buf);
		return NULL;
	}
	do {
		size_t l = strlen(buf);
		if ((l > 0) && (buf[l-1] == '\n')) {
			l--;
			buf[l] = 0;
			return buf;
		}
		if (buf_len >= (10000 / 2)) printf("erreur d'allocation lors du transfert.");
		buf_len *= 2;
		buf = xrealloc(buf, buf_len * sizeof(char));
		if (fgets(buf + l, buf_len - l, stdin) == NULL) return buf;
	} while (1);
}

/*
 * @param token:le string saisi par le client;
 * cmd:l'adresse de la commande reconnue
 */
void get_command(STRING token, COMMAND* cmd) {
    COMMAND i;
    if (token == NULL)return;
    for (i = 0; i < NB_CMD; i++) {
        if (!strcmp(token, commands[i])) break;
    }
    if (i < NB_CMD) /* si la commande a ete trouvee */
        *cmd = i;
    else *cmd = ERROR; /* sinon on signale l'erreur*/
}

/*
 * @param args:le string net apres la commande; 
 * pcmd:l'adresse de la commande
 * @return le nom du fichier a recuperer(GET) ou a envoyer(PUT)
 */
STRING get_args(STRING args, COMMAND* pcmd) {
    STRING nomfichier;
    /* en fonction de la commande desiree */
    switch (*pcmd) {
            /* pour les 2 cas, meme code est executer */
        case PUT:
        case GET:
            if (args == NULL || !strcmp(args, "")) {
                /* erreur si aucun argument */
                *pcmd = ERROR; printf("\nerreur de saisie des arguments de la commande\n");
		return "";
            } else {
                nomfichier = args;
            }
            break;
	case LS: 
        default: ;
    }
    return nomfichier;
}

/*
 * @param l'adresse du nom du fichier a recuperer(GET) ou a envoyer(PUT)
 * @return la commande saisie par le client
 */
COMMAND read_command(STRING nomfichier) {
    COMMAND cmdNette;
    char* cmdBrutte;
    STRING token;
    cmdBrutte=readline();
    if (strlen (cmdBrutte)==0) return ERROR;
    token = strtok(cmdBrutte, " "); // recuperation de la commande
    get_command(token, &cmdNette); // determination de la commande
    //si la commande a ete correctement identifiee, on obtient les arguments :
    if (cmdNette != ERROR && cmdNette !=LS) {
        token = strtok(NULL, "\n");
        strcpy(nomfichier, get_args(token, &cmdNette));
    }
    return cmdNette;
}

/*
 * programme du client
 */
int main(int argc, char* argv[]) {
    int continueRunning = 1;
    int firstTime=1;
 while (continueRunning) {
    COMMAND cmd;
    char nomfichier[FILE_NAME_MAX_SIZE];
    STRING nomProgramme = argv[0];
    STRING stringServeur;
    STRING stringPort;
    int numPort;
    char* endptr = (char*) 1;
    char buffer[BUFFER_SIZE];
    int length;
    int file_block_length;
    int erreur;
const char* ls= "ls";
     if (firstTime)
     printf("\n============================\n"
              "=========  CLIENT  =========\n"
              "============================\n");

	if ((argc > 1 && !strcmp(argv[1], "help"))) {// menu d'aide
	    helpProgramme(nomProgramme);
	    return 1;
	}
	if (argc > 3) {// trop d'argument
	    if ((argc >= 1 && !strcmp(argv[0], "help"))) {
		printf("\n/!\\Attention erreur/!\\: nombre de parametres incorect.\n");
	    }
	    helpProgramme(nomProgramme);
	    return 1;
	} else {
	    if (argc != 3) { // valeurs par défault
		stringServeur = ADR_DEFAULT;
		stringPort = PORT_DEFAULT;
	    } else {// Si les valeurs sont définies dans les arguments
		stringServeur = argv[1];
		stringPort = argv[2];
	    }
	}

    /* string du port => numero du port */
    numPort = strtol(stringPort, &endptr, 0);

    if ((*endptr != '\0') || (numPort < 1000) || (numPort >= 100000)) {
        /* erreur si le numero de port n'est pas entier
         ou s'il est un port de bas niveau, ou s'il est un port de tres haut niveau */
        printf("\n/!\\Attention erreur/!\\: numero de port invalide.\n");
        helpProgramme(nomProgramme);
        return 0;
    }

    /* structure de l'adresse du client */
    SOCKADDR_IN client_addr;
    bzero(&client_addr, sizeof (client_addr));
    client_addr.sin_family = AF_INET; // famille des protocoles internet
    client_addr.sin_addr.s_addr = htons(INADDR_ANY); // recuperer adresse du hote local
    client_addr.sin_port = htons(0); // attribuer un port libre

    /* creer TCP socket du client */
    SOCKET client_socket = socket(PF_INET, SOCK_STREAM, 0);
    if (client_socket < 0) {
        printf("\n/!\\Attention erreur/!\\: erreur dans la creation de socket.\n");
        return 0;
    }

    /* bind socket et l'adresse du socket */
    if (bind(client_socket, (SOCKADDR*) & client_addr, sizeof (client_addr))) {
        printf("\n/!\\Attention erreur/!\\: bind port du client.\n");
        return 0;
    }

    /* structure de l'adresse du serveur */
    SOCKADDR_IN server_addr;
    bzero(&server_addr, sizeof (server_addr));
    server_addr.sin_family = AF_INET;

    /* verifier l'adresse du serveur */
    if (inet_aton(stringServeur, &server_addr.sin_addr) == 0) {
        printf("\n/!\\Attention erreur/!\\: adresse du serveur introuvable.\n");
        return 0;
    }

    server_addr.sin_port = htons(numPort);
    socklen_t server_addr_length = sizeof (server_addr);

    /* demande de connexion au serveur */
    if (connect(client_socket, (SOCKADDR*) & server_addr, server_addr_length) < 0) {
        printf("\n/!\\Attention erreur/!\\: connexion impossible au %s.\n", stringServeur);
    }

    /* connexion etablie */
    if(firstTime){
     printf("\n");
     printf("Connexion etablie:\n");
     printf("Serveur: %s\n", stringServeur);
     printf("Port: %d\n", numPort);
     printf("\n");
     firstTime=0;
    }

    /* shell du client */
   
        printf(PROMPT);
        cmd = read_command(nomfichier);
        switch (cmd) {
            case HELP:
                helpShell();
                break;

            case LS:

                //envoyer la commande LS au serveur
                bzero(buffer, sizeof (buffer));
                strncpy(buffer, commands[LS], strlen(commands[LS]) > BUFFER_SIZE ? BUFFER_SIZE : strlen(commands[LS]));
                send(client_socket, buffer, BUFFER_SIZE, 0);

                //recevoir le resultat de "ls" du serveur et l'afficher
                bzero(buffer, sizeof (buffer));
                length = 0;
                printf("\n====Les fichiers du serveur====\n");
                while ((length = recv(client_socket, buffer, BUFFER_SIZE, 0))) {
                    if (length < 0) {
                        printf("\n/!\\Attention erreur/!\\: reception des donnees du serveur.\n");
                        break;
                    }
                    printf("%s", buffer);
                    bzero(buffer, sizeof (buffer));
                }
                printf("\n====Les fichiers locaux:=====\n");
		system(ls);
                printf("\n");
		break;

            case PUT:
                //envoyer la commande PUT au serveur
                bzero(buffer, sizeof (buffer));
                strncpy(buffer, commands[PUT], strlen(commands[PUT]) > BUFFER_SIZE ? BUFFER_SIZE : strlen(commands[PUT]));
                send(client_socket, buffer, BUFFER_SIZE, 0);

                //ouvrir le fichier local
                FILE* fp_put = fopen(nomfichier, "r");
                if (fp_put == NULL) {
                    printf("\n!Erreur:  Le fichier %s n'existe pas.\n", nomfichier);
                    break;
                }

                //envoyer le nom du fichier au serveur
                bzero(buffer, sizeof (buffer));
                strncpy(buffer, nomfichier, strlen(nomfichier) > BUFFER_SIZE ? BUFFER_SIZE : strlen(nomfichier));
                send(client_socket, buffer, BUFFER_SIZE, 0);

                //envoyer le fichier au serveur
                bzero(buffer, sizeof (buffer));
                file_block_length = 0;
                erreur = 0;
                while ((file_block_length = fread(buffer, sizeof (char), BUFFER_SIZE, fp_put)) > 0) {
                    if (send(client_socket, buffer, file_block_length, 0) < 0) {
                        printf("\nErreur: le fichier n'a pas pu etre envoyé %s.\n", nomfichier);
                        erreur = 1;
                        break;
                    }

                    bzero(buffer, sizeof (buffer));
                }
                fclose(fp_put);
                if (!erreur) printf("\nLe Fichier %s a ete envoye.\n", nomfichier);
                break;

            case GET:
                //envoyer la commande au serveur
                bzero(buffer, sizeof (buffer));
                strncpy(buffer, commands[GET], strlen(commands[GET]) > BUFFER_SIZE ? BUFFER_SIZE : strlen(commands[GET]));
                send(client_socket, buffer, BUFFER_SIZE, 0);

                //envoyer le nom du fichier au serveur
                bzero(buffer, sizeof (buffer));
                strncpy(buffer, nomfichier, strlen(nomfichier) > BUFFER_SIZE ? BUFFER_SIZE : strlen(nomfichier));
                send(client_socket, buffer, BUFFER_SIZE, 0);

                //ouvrir le fichier local
                FILE* fp_get = fopen(nomfichier, "w");
                if (fp_get == NULL) {
                    printf("\nErreur: fichier %s ne peut pas etre ouvert et ecrit dedans.\n", nomfichier);
                    break;
                }

                //recevoir le fichier du serveur et ecrire dans le fichier local
                bzero(buffer, sizeof (buffer));
                length = 0;
                erreur = 0;
                while ((length = recv(client_socket, buffer, BUFFER_SIZE, 0))) {
                    if (!strcmp(buffer, "ERR_FICHIER_N_EXISTE_PAS")) {
                        printf("\nErreur: le fichier %s n'existe pas.\n", nomfichier);
			system(strcat("rm ",nomfichier));
                        erreur = 1;
                        break;
                    }
                    if (length < 0) {
                        printf("\nErreur: reception des donnees du serveur.\n");
                        erreur = 1;
                        break;
                    }
                    int write_length = fwrite(buffer, sizeof (char), length, fp_get);
                    if (write_length < length) {
                        printf("\nErreur: ecriture du fichier %s.\n", nomfichier);
                        erreur = 1;
                        break;
                    }
                    bzero(buffer, sizeof (buffer));
                }
                fclose(fp_get);

                if (!erreur) printf("\nFichier %s recu du serveur.\n", nomfichier);
                else system(strcat("rm -f ", nomfichier)); //si erreur, supprimer le fichier creer

                break;

            case QUIT:
                //mettre fin au programme
                continueRunning = 0;
		return 0;
                break;

            case ERROR:
                printf("\nErreur: commande incorrecte.\n");
                break;

            default:;
        }
    
    close(client_socket); //fermer socket}
   
} return 0;}
